package com.zycoin.common.plugin.mybatis.page;

import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.RowBounds;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Properties;

@Intercepts({@Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class, Integer.class})})
public class TPageInterceptor implements Interceptor {

    private static final Log logger = LogFactory.getLog(TPageInterceptor.class);

    private String dialect;

    /**
     * 拦截后要执行的方法
     */
    public Object intercept(Invocation invocation) throws Throwable {
        RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(handler);
        // 先判断是不是SELECT操作
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        if (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {
            return invocation.proceed();
        }
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");

        Object parameterObject = boundSql.getParameterObject();
        if (null == parameterObject)
            return invocation.proceed();

        if (parameterObject instanceof  BaseEntity) {
            BaseEntity baseEntity = (BaseEntity) parameterObject;

            Page page = baseEntity.getPageMap();
            if (page.getStartNum() >= 0) {
                // 拦截到的prepare方法参数是一个Connection对象
                Connection connection = (Connection) invocation.getArgs()[0];
                // 获取当前要执行的Sql语句，也就是我们直接在Mapper映射语句中写的Sql语句
                String sql = boundSql.getSql();
                // 给当前的page参数对象设置总记录数，已经设置过的不再设置
                if (page.getTotalRecord() < 0) {
                    page.setTotalRecord(this.getTotalRecord(boundSql, mappedStatement, connection, metaObject));
                }
                // 获取分页Sql语句
                String pageSql = this.getPageSql(page, sql);
                // 设置当前BoundSql对应的sql属性为我们建立好的分页Sql语句
                metaObject.setValue("delegate.boundSql.sql", pageSql);
                // 禁用内存分页
                metaObject.setValue("delegate.rowBounds.offset", RowBounds.NO_ROW_OFFSET);
                metaObject.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);
            }
        }
        return invocation.proceed();
    }

    /**
     * 拦截器对应的封装原始对象的方法
     */
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    /**
     * 设置注册拦截器时设定的属性
     */
    public void setProperties(Properties properties) {
        this.dialect = properties.getProperty("dialect");
    }

    /**
     * 根据page对象获取对应的分页查询Sql语句，这里只做了两种数据库类型，Mysql和Oracle 其它的数据库都 没有进行分页
     *
     * @param page 分页对象
     * @param sql  原sql语句
     * @return
     */
    private String getPageSql(Page page, String sql) {
        StringBuffer sqlBuffer = new StringBuffer(sql);
        if ("mysql".equalsIgnoreCase(dialect)) {
            return getMysqlPageSql(page, sqlBuffer);
        } else if ("oracle".equalsIgnoreCase(dialect)) {
            return getOraclePageSql(page, sqlBuffer);
        }
        return sqlBuffer.toString();
    }

    /**
     * 获取Mysql数据库的分页查询语句
     *
     * @param page      分页对象
     * @param sqlBuffer 包含原sql语句的StringBuffer对象
     * @return Mysql数据库分页语句
     */
    private String getMysqlPageSql(Page page, StringBuffer sqlBuffer) {
        // 计算第一条记录的位置，Mysql中记录的位置是从0开始的。
        sqlBuffer.append(" limit ").append(page.getStartNum()).append(",").append(page.getPageSize());
        return sqlBuffer.toString();
    }

    /**
     * 获取Oracle数据库的分页查询语句
     *
     * @param page      分页对象
     * @param sqlBuffer 包含原sql语句的StringBuffer对象
     * @return Oracle数据库的分页查询语句
     */
    private String getOraclePageSql(Page page, StringBuffer sqlBuffer) {
        // 计算第一条记录的位置，Oracle分页是通过rownum进行的，而rownum是从1开始的
        sqlBuffer.insert(0, "select u.*, rownum row_num from (").append(") u where rownum <=").append(page.getStartNum() + page.getPageSize());
        sqlBuffer.insert(0, "select * from (").append(") where row_num > ").append(page.getStartNum());
        // 上面的Sql语句拼接之后大概是这个样子：
        // select * from (select u.*, rownum r from (select * from t_user) u
        // where rownum < 31) where r >= 16
        return sqlBuffer.toString();
    }

    /**
     * 获取总查询总条数
     *
     * @param boundSql
     * @param mappedStatement Mapper映射语句
     * @param connection      当前的数据库连接
     */
    private Integer getTotalRecord(BoundSql boundSql, MappedStatement mappedStatement, Connection connection, MetaObject metaStatementHandler) {
        // 获取到我们自己写在Mapper映射语句中对应的Sql语句
        String countSql = this.getCountSql(boundSql.getSql());
        Integer totalRecord = 0;
        // 通过connection建立一个countSql对应的PreparedStatement对象。
        try(PreparedStatement pstmt= connection.prepareStatement(countSql)) {
            // 通过mappedStatement、参数对象page和BoundSql对象countBoundSql建立一个用于设定参数的ParameterHandler对象
            ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), boundSql);
            // 通过parameterHandler给PreparedStatement对象设置参数
            parameterHandler.setParameters(pstmt);
            // 之后就是执行获取总记录数的Sql语句和获取结果了。
            try (ResultSet rs = pstmt.executeQuery()){
                if (rs.next()) {
                    totalRecord = rs.getInt(1);
                }
            }
        } catch (Exception e){
            logger.error("Error: Method getTotalRecord execution error !", e);
        }
        return totalRecord;
    }

    /**
     * 根据原Sql语句获取对应的查询总记录数的Sql语句
     *
     * @param sql
     * @return
     */
    private String getCountSql(String sql) {
        StringBuffer sb = new StringBuffer();
        sb.append("select count(1) from (").append(sql).append(") countNum");
        return sb.toString();
    }
}
